Перейти к основному содержимому

5.05. Коллекции

Разработчику Архитектору

Коллекции

В реальной разработке редко приходится работать с одним объектом. Чаще всего — у нас есть список пользователей, набор заказов, список настроек, очередь задач и т.д. Чтобы эффективно хранить, обрабатывать и управлять такими группами, в C# существует мощная система коллекций.

Коллекция — это структура данных, предназначенная для хранения набора элементов одного или разных типов. Она позволяет добавлять и удалять элементы, перебирать элементы, искать, сортировать, фильтровать, организовывать данные по определённому принципу (по индексу, по ключу, по приоритету и т.д.).

class User
{
public int Id { get; set; }
public string Name { get; set; }
}

// Создаём коллекцию пользователей
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 2, Name = "Bob" },
new User { Id = 3, Name = "Charlie" }
};

// Обрабатываем всех пользователей
foreach (var user in users)
{
Console.WriteLine($"Hello, {user.Name}!");
}

Без коллекций пришлось бы писать user1, user2, user3… — и быстро запутаться. А что если количество заранее неизвестно, и понадобится делать запрос в базу данных - и там может быть как один user, так и тысяча?

Собственно, поэтому коллекции являются основой для такой работы. И они бывают разные, в зависимости от необходимости.

  • Хранить много объектов - List<User>;
  • Быстро находить по ID - Dictionary<int, User>;
  • Избежать дубликатов - HashSet<User>;
  • Обрабатывать по порядку (FIFO) - Queue<Task>;
  • Обрабатывать «последний — первый» (LIFO) - Stack<UndoAction>;
  • Группировать данные - ILookup<Department, Employee>.

В .NET все коллекции строятся на основе иерархии интерфейсов. Это позволяет писать гибкий, расширяемый и тестируемый код.

ИнтерфейсНазначение
IEnumerable<T>Поддержка перебора элементов (например, с помощью foreach)
ICollection<T>Предоставляет возможности для добавления, удаления и подсчёта элементов
IList<T>Обеспечивает доступ к элементам по индексу (list[0]) и поддержку упорядоченного списка
IDictionary<TKey, TValue>Хранение и управление коллекцией пар «ключ-значение»
ISet<T>Представляет математическое множество — коллекцию без дублирующихся элементов
IReadOnlyCollection<T>Коллекция только для чтения, с возможностью получения количества элементов
IReadOnlyList<T>Список только для чтения, с доступом к элементам по индексу

IEnumerable<T> - лучше всего подходит для переборов. Позволяет использовать foreach, ковариантен (out T) - можно присвоить IEnumerable<string> переменной IEnumerable<object>. Реализуется всеми коллекциями.

public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}

Пример:

IEnumerable<string> names = new List<string> { "Alice", "Bob" };
foreach (string name in names) { ... }

yield return — ключевое слово для ленивого перебора:

public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}

IEnumerator<T> - это итератор. Он управляет текущим элементом при переборе.

public interface IEnumerator<out T> : IDisposable
{
T Current { get; }
bool MoveNext(); // Переходит к следующему элементу
void Reset(); // Сбрасывает (редко используется)
}

foreach под капотом использует GetEnumerator() и MoveNext().

ICollection<T> используется для изменяемых коллекций. Он наследует IEnumerable<T> и добавляет операции Add, Remove, Count.

public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
bool Remove(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
}

IList<T> используется для доступа по индексу. Позволяет list[0] = item; и реализуется List<T>, T[].

public interface IList<T> : ICollection<T>
{
T this[int index] { get; set; } // Индексатор
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}

IDictionary<TKey, TValue> - это словарь. Он хранит пары «ключ - значение» и позволяет получать быстрый доступ по ключу (O(1) в среднем).

public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
bool ContainsKey(TKey key);
bool TryGetValue(TKey key, out TValue value);
void Add(TKey key, TValue value);
bool Remove(TKey key);
}

ISet<T> используется как множество без дубликатов, гарантируя уникальность элементов. Реализуется HashSet<T>, SortedSet<T>.

public interface ISet<T>
{
bool Add(T item); // Возвращает false, если уже есть
void UnionWith(IEnumerable<T> other);
void IntersectWith(IEnumerable<T> other);
bool IsSubsetOf(IEnumerable<T> other);
}

IReadOnlyCollection<T> и IReadOnlyList<T> нужны для защиты от изменений. Они используются, когда нужно только читать данные.

public interface IReadOnlyCollection<out T> : IEnumerable<T>
{
int Count { get; }
}

public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
{
T this[int index] { get; }
}

Есть обобщённые и необобщённые коллекции. Необобщённые нетипобезопасны, требуют приведения типов и заметно медленнее из-за распаковки/упаковки. Необобщённые находятся в System.Collections.

Необобщённые (устаревшие):

  • ArrayList - Список объектов object;
  • Hashtable - Словарь объектов;
  • Queue – Очередь;
  • Stack – Стек;
  • SortedList - Отсортированный список

Обобщённые:

  • List<T> - упорядоченная коллекция элементов;
  • Dictionary<TKey, TValue> - словарь «ключ-значение»;
  • HashSet<T> - множество без дубликатов;
  • LinkedList<T> - связный список;
  • Queue<T> - FIFO (первый вошел – первый вышел);
  • Stack<T> - LIFO (последний вошел – первый вышел);
  • SortedSet<T> - отсортированное множество;
  • ConcurrentBag<T> - Параллельная коллекция.

Параллельные (потокобезопасные) коллекции (из System.Collections.Concurrent):

  • ConcurrentQueue<T>
  • ConcurrentStack<T>
  • ConcurrentDictionary<TKey, TValue>

Потокобезопасные (параллельные) коллекции — это специальные структуры данных, разработанные для безопасной работы из нескольких потоков одновременно. Обычные коллекции, такие как List<T>, Dictionary<TKey, TValue>, не являются потокобезопасными. Если к ним обращаются несколько потоков (особенно с записью), возможны повреждения структуры данных, исключения, гонки данных. Чтобы этого избежать, можно использовать блокировки (lock), но это медленно, сложно в отладке и может привести к взаимоблокировкам (deadlock).

Потокобезопасная коллекция позволяет получать одновременный доступ из нескольких потоков, гарантирует целостность данных, обеспечивает атомарность операций и не требует ручных блокировок в большинстве случаев. Они используют низкоуровневые примитивы (например, Interlocked, SpinWait) для эффективной синхронизации.

Если у вас один поток - используйте обычные коллекции, они быстрее. Также не стоит использовать параллельные коллекции, когда нужен порядок и сортировка, ведь они не гарантируют порядок.